package ru.papergames.battleserver.model.battle.services;

import org.apache.log4j.Logger;
import ru.common.util.Point3D;
import ru.papergames.battleserver.model.PaperGameNpcUser;
import ru.papergames.battleserver.model.battle.BallColor;
import ru.papergames.battleserver.model.battle.GameBall;
import ru.papergames.battleserver.model.battle.GameBallsLine;
import ru.papergames.battleserver.model.battle.map.BattleMap;
import ru.papergames.battleserver.model.battle.map.Cell;
import ru.papergames.battleserver.model.battle.npc.*;

import java.util.*;

/**
 *
 * TODO: create tests for this calculator!
 *
 * @author : atroshanin
 */
public class DefaultNpcDataCalculatorImpl implements NpcDataCalculator {

    private static final Logger log = Logger.getLogger(DefaultNpcDataCalculatorImpl.class);

    private static DefaultNpcDataCalculatorImpl instance = new DefaultNpcDataCalculatorImpl();

    private static final int [][] NEIGHBOURS = new int[][] {
            {-1,    0},

            {-1,   -1},
            { 0,   -1},
            { 1,   -1},

            { 1,    0},

            { 1,    1},
            { 0,    1},
            {-1,    1}
    };

    public static DefaultNpcDataCalculatorImpl getInstance() {
        return instance;
    }

    private DefaultNpcDataCalculatorImpl() {
    }

    @Override
    public void handleBallsThrow(PaperGameNpcUser npcUser, List<GameBall> newBalls) {
        NpcDataContainer dataContainer = npcUser.getDataContainer();
        NpcBattleMap map = dataContainer.getMap();
        BallColor npcColor = npcUser.getBattleMember().getBallColor();
        for (GameBall gameBall : newBalls) {
            int x = gameBall.getCell().getX();
            int y = gameBall.getCell().getY();
            NpcBall npcBall= new NpcBall(gameBall.getColor());
            dataContainer.addNpcBall(npcBall);
            processBallAddedToCell(dataContainer, map, map.getCell(x, y), npcBall, npcColor);
        }

    }

    private void processBallAddedToCell(NpcDataContainer dataContainer, NpcBattleMap map, NpcCell npcCell, NpcBall npcBall, BallColor npcColor) {
        map.addNpcBall(npcCell, npcBall);

        if (npcBall.getColor() == npcColor) {
            // trying to find new lines
            // determine lines if ball is npc's ball
            for (LineVector vector : LineVector.values()) {
                checkLineForVector(map, npcBall, dataContainer, vector);
            }
        }

        // determine if new blocked balls appeared
        checkBlockedCells(map, npcBall, dataContainer, npcColor);
    }

    private void checkBlockedCells(NpcBattleMap map, NpcBall newNpcBall, NpcDataContainer dataContainer,  BallColor npcColor) {
        int x0 = newNpcBall.getCell().getX();
        int y0 = newNpcBall.getCell().getY();
        int busyCellsCount = 0;

        //----
        for (LineVector vector : LineVector.values()) {
            NpcCell c1 = map.getCell(x0 + vector.getDx(), y0 + vector.getDy());
            NpcCell c2 = map.getCell(x0 - vector.getDx(), y0 - vector.getDy());
            busyCellsCount += checkNeighbourCellIsBlocked(map, c1, vector, dataContainer, npcColor);
            busyCellsCount += checkNeighbourCellIsBlocked(map, c2, vector, dataContainer, npcColor);
        }
        //----

        /*for (int [] neighbourPointer : NEIGHBOURS) {
            int dx = neighbourPointer[0];
            int dy = neighbourPointer[1];
            NpcCell cell = map.getCell(x0 + dx, y0 + dy);
            if (cell != null && !cell.isCellFree()) {
                busyCellsCount++;
                NpcBall ball = cell.getNpcBall();
                if (ball != null && ball.getColor() == npcColor) {
                    checkCellIsBlocked(ball, map, dataContainer);
                }
            }
        } */
        if (npcColor == newNpcBall.getColor()) {
            dataContainer.setNpcBallBlockStatus(newNpcBall, busyCellsCount == 8);
        }
    }

    private int checkNeighbourCellIsBlocked(NpcBattleMap map, NpcCell cell, LineVector vector, NpcDataContainer dataContainer,  BallColor npcColor) {
        if (cell != null && !cell.isCellFree()) {
            NpcBall ball = cell.getNpcBall();
            if (ball != null && ball.getColor() == npcColor) {
                checkCellIsBlocked(ball, map, dataContainer);

                NpcLine ballLine = ball.getLine(vector);
                if (ballLine != null && ballLine.isLineBlocked()) {
                    dataContainer.setNpcLineBlockStatus(ballLine, true);
                }
            }
            return 1;
        }
        return 0;
    }

    private void checkCellIsBlocked(NpcBall npcBall, NpcBattleMap map, NpcDataContainer dataContainer) {
        int x0 = npcBall.getCell().getX();
        int y0 = npcBall.getCell().getY();
        for (int [] neighbourPointer : NEIGHBOURS) {
            int dx = neighbourPointer[0];
            int dy = neighbourPointer[1];
            NpcCell neighbourCell = map.getCell(x0 + dx, y0 + dy);
            if (neighbourCell != null && neighbourCell.isCellFree()) {
                return;
            }
        }
        dataContainer.setNpcBallBlockStatus(npcBall, true);
    }

    /**
     * Here we try to check if there are neighbour cells with current NPC color ball so we can create new line
     * or make longer existing
     * @param map - npc map
     * @param newNpcBall - new ball for npc
     * @param dataContainer - npc data container
     * @param vector - line's vector
     */
    private void checkLineForVector(NpcBattleMap map, NpcBall newNpcBall, NpcDataContainer dataContainer, LineVector vector) {
        int x0 = newNpcBall.getCell().getX();
        int y0 = newNpcBall.getCell().getY();
        NpcCell c1 = map.getCell(x0 + vector.getDx(), y0 + vector.getDy());
        checkLineForCell(c1, newNpcBall, dataContainer, vector);
        NpcCell c2 = map.getCell(x0 - vector.getDx(), y0 - vector.getDy());
        checkLineForCell(c2, newNpcBall, dataContainer, vector);
    }

    private void checkLineForCell(NpcCell neighbourCell, NpcBall newNpcBall, NpcDataContainer dataContainer, LineVector vector) {
        if (neighbourCell != null) {
            NpcBall neighbourNpcBall = neighbourCell.getNpcBall();
            if (neighbourNpcBall != null && neighbourNpcBall.getColor() == newNpcBall.getColor()) {

                // 1 check if there is existing line in existing ball && in new ball
                NpcLine neighbourBallLine = neighbourNpcBall.getLine(vector);
                NpcLine newBallLine = newNpcBall.getLine(vector);
                NpcLine resultLine;
                if (neighbourBallLine != null) {

                    if (newBallLine == null) {
                        // adding new ball to line of existing ball.
                        resultLine = addBallToLine(neighbourBallLine, newNpcBall);
                    } else {
                        // merging lines
                        resultLine = mergeLines(neighbourBallLine, newBallLine, dataContainer);
                    }
                } else {
                    if (newBallLine == null) {
                        // creating new line and add these two balls to it
                        NpcLine line = new NpcLine(dataContainer.getMap(), vector);
                        addBallToLine(line, newNpcBall);
                        addBallToLine(line, neighbourNpcBall);
                        dataContainer.addLine(line);
                        resultLine = line;
                    } else {
                        resultLine = addBallToLine(newBallLine, neighbourNpcBall);
                    }
                }
                dataContainer.setNpcLineBlockStatus(resultLine, resultLine.isLineBlocked());
            }
        }
    }

    private NpcLine addBallToLine(NpcLine line, NpcBall ball) {
        line.addNpcBall(ball);
        ball.setLine(line);
        return line;
    }

    private NpcLine addBallsToLine(NpcLine line, Set<NpcBall> balls) {
        line.addNpcBalls(balls);
        return line;
    }

    private NpcLine mergeLines(NpcLine lineDst, NpcLine lineSrc, NpcDataContainer dataContainer) {
        if (lineDst.getBalls().size() < lineSrc.getBalls().size()) {
            NpcLine q = lineDst;
            lineDst = lineSrc;
            lineSrc = q;
        }
        addBallsToLine(lineDst, lineSrc.getBalls());
        dataContainer.removeLine(lineSrc);
        return lineDst;
    }

    @Override
    public void handleBallMoved(PaperGameNpcUser npcUser, List<Point3D> ballRoute) {
        NpcDataContainer dataContainer = npcUser.getDataContainer();
        NpcBattleMap map = dataContainer.getMap();
        BallColor npcBallColor = npcUser.getBattleMember().getBallColor();

        NpcCell startCell = map.getCell(ballRoute.get(0));
        if (startCell == null) {
            log.error("[ALARM], there is no npc cell for start point in route " + ballRoute);
            return;
        }
        if (startCell.getNpcBall() == null) {
            log.error("[ALARM], there is no npc ball for start point in route " + ballRoute);
            return;
        }
        NpcCell endCell = map.getCell(ballRoute.get(ballRoute.size() - 1));
        if (endCell == null) {
            log.error("[ALARM], there is no npc cell for end point in route " + ballRoute);
            return;
        }
        if (endCell.getNpcBall() != null) {
            log.error("[ALARM], there is ALREADY npc ball in end point in route " + ballRoute);
            return;
        }

        NpcBall ballToMove = startCell.getNpcBall();
        BallColor ballColor = ballToMove.getColor();

        if (npcBallColor == ballColor) {
            processLinesForRemovedNpcBall(dataContainer, ballToMove, null, null);
        }
        map.removeNpcBall(ballToMove.getCell());
        checkUnlockedCells(dataContainer, map, startCell.getX(), startCell.getY(), npcBallColor);

        processBallAddedToCell(dataContainer, map, endCell, ballToMove, npcBallColor);
    }

    @Override
    public void handleBallsRemoved(PaperGameNpcUser npcUser, Set<Cell> cellsToBeFree, List<GameBallsLine> lines, BallColor ballsColor) {
        BattleMap battleMap = npcUser.getBattle().getBattleMap();
        NpcDataContainer dataContainer = npcUser.getDataContainer();
        NpcBattleMap map = dataContainer.getMap();

        BallColor npcBallColor = npcUser.getBattleMember().getBallColor();
        for (Cell cellToBeFree : cellsToBeFree) {

            NpcCell npcCell = map.getCell(cellToBeFree.getX(), cellToBeFree.getY());
            NpcBall npcBall = npcCell.getNpcBall();
            // 1. take npc cell and analyze each line
            if (npcBallColor == ballsColor) {
                // ONLY IF Ball color is color of current npc!!!
                if (npcBall == null) {
                    log.error("ALARM, npc cell does not have ball but real map has it!");
                    continue;
                }
                processLinesForRemovedNpcBall(dataContainer, npcBall, cellsToBeFree, battleMap);

            }
            // 3. removing npc ball from container & from map
            dataContainer.removeNpcBall(npcBall);
            map.removeNpcBall(npcCell);

            // 4. for any npc we check unblocked cells
            checkUnlockedCells(dataContainer, map, cellToBeFree.getX(), cellToBeFree.getY(), npcBallColor);
        }
    }

    private void checkUnlockedCells(NpcDataContainer dataContainer, NpcBattleMap map, int x0, int y0, BallColor npcBallColor) {
        for (LineVector vector : LineVector.values()) {
            NpcCell c1 = map.getCell(x0 + vector.getDx(), y0 + vector.getDy());
            NpcCell c2 = map.getCell(x0 - vector.getDx(), y0 - vector.getDy());
            checkNeighbourCellIsUnlocked(c1, vector, dataContainer, npcBallColor);
            checkNeighbourCellIsUnlocked(c2, vector, dataContainer, npcBallColor);
        }

        /*
        for (int [] neighbourPointer : NEIGHBOURS) {
            int dx = neighbourPointer[0];
            int dy = neighbourPointer[1];
            NpcCell neighbourCell = map.getCell(x0 + dx, y0 + dy);
            NpcBall npcBall = neighbourCell != null ? neighbourCell.getNpcBall() : null;
            if (npcBall != null && npcBall.getColor() == npcBallColor && npcBall.isBlocked()) {
                dataContainer.setNpcBallBlockStatus(npcBall, false);
            }
        }
        */
    }

    private void checkNeighbourCellIsUnlocked(NpcCell neighbourCell, LineVector vector, NpcDataContainer dataContainer, BallColor npcBallColor) {
        NpcBall npcBall = neighbourCell != null ? neighbourCell.getNpcBall() : null;
        if (npcBall != null && npcBall.getColor() == npcBallColor) {
            if (npcBall.isBlocked()) {
                dataContainer.setNpcBallBlockStatus(npcBall, false);
            }
            NpcLine line = npcBall.getLine(vector);
            if (line != null) {
                dataContainer.setNpcLineBlockStatus(line, false);
            }
        }
    }

    private void processLinesForRemovedNpcBall(NpcDataContainer dataContainer, NpcBall npcBall, Set<Cell> cellsToBeFree, BattleMap battleMap) {
        for (LineVector vector : LineVector.values()) {
            NpcLine line = npcBall.getLine(vector);
            if (line == null) {
                continue;
            }
            removeBallFromLine(line, npcBall);
            NpcBall oneRemainedInLine = null;
            List<NpcBall> ballsToBeRemoved = new LinkedList<NpcBall>();
            if (cellsToBeFree != null) {
                for (NpcBall npcBallInLine : line.getBalls()) {
                    int x = npcBallInLine.getCell().getX();
                    int y = npcBallInLine.getCell().getY();
                    if (cellsToBeFree.contains(battleMap.getCell(x, y))) {
                        // here we determine that current ball in line is going to be removed, so we can remove this ball from line
                        ballsToBeRemoved.add(npcBallInLine);
                        npcBallInLine.removeLine(vector);
                    } else {
                        oneRemainedInLine = npcBallInLine;
                    }
                }
            }
            // 2. If all line is going to be removed, then removing line else splitting it
            if (ballsToBeRemoved.size() == line.getBalls().size()) {
                dataContainer.removeLine(line); // on this line there are no references from npc balls (only this line contains references for balls but it does not matter) - we can simply remove it and GC will do its work
            } else {
                switch (line.getBalls().size()) {
                    case 1:
                        oneRemainedInLine = (oneRemainedInLine == null) ? line.getBalls().iterator().next() : oneRemainedInLine;
                        removeBallFromLine(line, oneRemainedInLine);
                    case 0:
                        dataContainer.removeLine(line);
                        break;
                    default: {
                        // VERY RARE SITUATION!!!
                        dataContainer.removeLine(line);
                        line.removeNpcBalls(ballsToBeRemoved);
                        generateLines(dataContainer, line.getBalls(), vector);
                    }
                }
            }
        }
    }

    private void generateLines(NpcDataContainer dataContainer, Collection<NpcBall> npcBalls, LineVector vector) {
        ArrayList<NpcBall> sortedBalls = new ArrayList<NpcBall>(npcBalls);
        Collections.sort(sortedBalls, new Comparator<NpcBall>() {
            @Override
            public int compare(NpcBall b1, NpcBall b2) {
                int dx = b1.getCell().getX() - b2.getCell().getX();
                if (dx != 0) {
                    return dx;
                }
                return b1.getCell().getY() - b2.getCell().getY();
            }
        });
        // then trying to find neighbour cells in sorted array list
        List<NpcLine> newLines = new LinkedList<NpcLine>();
        NpcLine curLine = null;
        NpcBall prevBall = null;
        for (NpcBall npcBall : sortedBalls) {
            npcBall.removeLine(vector);
            if (prevBall != null) {
                if (isNeighbours(npcBall, prevBall)) {
                    if (curLine == null) {
                        curLine = new NpcLine(dataContainer.getMap(), vector);
                        curLine.addNpcBall(prevBall);
                    }
                    curLine.addNpcBall(npcBall);
                } else {
                    if (curLine != null) {
                        newLines.add(curLine);
                    }
                    curLine = null;
                }
            }
            prevBall = npcBall;
        }
        if (curLine != null) {
            newLines.add(curLine);
        }
        for (NpcLine newLine : newLines) {
            for (NpcBall b : newLine.getBalls()) {
                b.setLine(newLine);
            }
            dataContainer.addLine(newLine);
            dataContainer.setNpcLineBlockStatus(newLine, newLine.isLineBlocked());
        }
    }

    private boolean isNeighbours(NpcBall b1, NpcBall b2) {
        NpcCell c1 = b1.getCell();
        NpcCell c2 = b2.getCell();
        return Math.abs(c1.getX() - c2.getX()) == 1 || Math.abs(c1.getY() - c2.getY()) == 1;
    }

    private void removeBallFromLine(NpcLine line, NpcBall ball) {
        line.removeNpcBall(ball);
        ball.removeLine(line.getVector());
    }

}
